#FIXME!
from juegos._contests import *
from juegos.black_tablut_agent import *
from juegos.tablut import Move
from juegos.tablut_stats import *
from random import Random
import sys
import time

class TablutContest(Contest): ##########################################
    ''' All agents play count matches against all other agents, in all possible
        combinations. 
    '''
    GAMES_PER_CONTEST = 10
    
    def __init__(self, game, coefficients=None):
        Contest.__init__(self, game, [], TablutStats())
        self.matches = []
        for _ in xrange(TablutContest.GAMES_PER_CONTEST):
            self.matches.append((Tablut(), {Tablut.BLACK_PLAYER: BlackTablutAgent(coefficients), Tablut.WHITE_PLAYER: WhiteTablutAgent()}))
        
    def run(self):
        return Contest.run(self, self.matches)
    
class BlackTablutAgentEvolution(object):#############################################
    '''Representa la evolucion del agente negro. Implemeta todos los metodos necesarios y genera logs
       con informacion detallada de cada generacion de la evolucion. 
    '''
    INITIAL_POPULATION_SIZE = 20
    POPULATION_SIZE = 10
    INDIVIDUALS_TO_MATE = int(0.4*POPULATION_SIZE)
    MAX_GENERATIONS = 10
    MUTATION_VICTIMS = int(0.2*POPULATION_SIZE)
    COEFFICIENTS_INTERVAL = (0, 5000)
    RECALCULATE_INDIVIDUALS = True
    NON_MUTABLE_INDIVIDUALS = int(0*POPULATION_SIZE)
    INCLUDE_NEGATIVES = False
    
    def __init__(self, initial_population=None):
        self.max_fitness_values = []
        if initial_population:
            self.population = initial_population
        else:
            self.population = self._generate_population()
            self._next_generation_step_2()
    
    def _generate_population(self):
        '''Gerera la poblacion inicial con coeficientes aleatorios respetando las restricciones de los mismos.
        '''
        r = Random()
        population = []
        for _i in xrange(BlackTablutAgentEvolution.INITIAL_POPULATION_SIZE):
            coefficients = []
            for _j in xrange(BlackTablutAgent.AMOUNT_COEFFICIENTS):
                coefficients.append(r.randrange(BlackTablutAgentEvolution.COEFFICIENTS_INTERVAL[0], BlackTablutAgentEvolution.COEFFICIENTS_INTERVAL[1]))
            individual = (None, True, coefficients)
            population.append(individual)
        return population
    
    def select_parents(self):
        ''' Elije a los padres de manera que los individuos de mayor aptitud tengan mayores posibilidades.
            Utiliza el metodo de rueda de la fortuna.
        '''
        fitnesses = (individual[0] for individual in self.population)
        fitness_sum = float(sum(fitnesses))
        normalized_probabilities = [int(100*(individual[0]/fitness_sum)) for individual in self.population]
        choices = []
        for i in xrange(len(self.population)):
            choices += [self.population[i]]*normalized_probabilities[i]
        selected_individuals = []
        for _ in xrange(BlackTablutAgentEvolution.INDIVIDUALS_TO_MATE):
            if choices:
                ind = Random().choice(choices)
                selected_individuals.append(ind)
                choices = filter(lambda a: a != ind, choices) # remove all
        return selected_individuals
    
    def mate(self, a, b):
        ''' Cruza dos individuos, mediante cruzamiento generico. Asume que el largo de los vectores que
            representan individuos (en la posicion 2 de cada tupla) es de
            largo fijo.
        '''
        breaking_index = Random().randrange(1, len(a[2])-1)
        child1 = a[2][0:breaking_index]
        child1.extend(b[2][breaking_index:])
        child2 = b[2][0:breaking_index]
        child2.extend(a[2][breaking_index:])
        return [(None, True, child1), (None, True, child2)]
    
    def random_mate(self, individuals):
        ''' Cruza los individuos eligiendo las parejas de forma aleatoria y
            utilizando cada individuo en un unico apareamiento.
        '''
        shuffled_individuals = individuals[:]
        newborns = []
        Random().shuffle(shuffled_individuals)
        for i in xrange(len(shuffled_individuals)-1):
            newborns += self.mate(shuffled_individuals[i], shuffled_individuals[i+1])
        return newborns
    
    def cross_mate(self, individuals):
        ''' Promiscuamente cruza los individuos todos contra todos.  
        '''
        n = len(individuals)
        children = [self.mate(individuals[i], individuals[j]) for i in xrange(n) for j in xrange(n) if i!=j]
        return individuals + children
    
    def mutate(self, individual):
        ''' Cambia uno de los coeficientes por un entero aleatorio.
        '''
        r = Random()
        index1 = r.randrange(BlackTablutAgentEvolution.NON_MUTABLE_INDIVIDUALS, len(individual))
        if BlackTablutAgentEvolution.INCLUDE_NEGATIVES:
            individual[2][index1] = r.randrange(BlackTablutAgentEvolution.COEFFICIENTS_INTERVAL[0], BlackTablutAgentEvolution.COEFFICIENTS_INTERVAL[1])
        return (None, True, individual[2])
    
    def next_generation(self):
        ''' Avanza una generacion.
        '''
        r = Random()
        # seleccion de padres y engendracion de nuevos individuos
        print 'seleccionando padres...'
        selected_parents = self.select_parents()
        print 'cruzando...'
        newborns = self.random_mate(selected_parents)
        self.population = self.population+newborns
        # mutacion
        for _ in xrange(BlackTablutAgentEvolution.MUTATION_VICTIMS):
            # tiro una moneda y en caso de ser 1 muto un individuo
            if r.randint(0, 1):
                print 'mutando...'
                mutation_victim_index = r.randrange(0, len(self.population))
                self.population[mutation_victim_index] = self.mutate(self.population[mutation_victim_index])
        #FIXME!
        '''
        # Como la aptitud de los individuos es empirica, no queremos mantener falsos
        # mejores por haber salido favorecidos en el contest.
        for i in xrange(BlackTablutAgentEvolution.INDIVIDUALS_TO_RECALCULATE):
            j = Random().randrange(0, len(self.population))
            self.population[j] = (self.population[j][0], True, self.population[j][2])
        '''
        self._next_generation_step_2()
    
    def _next_generation_step_2(self):
        '''Segundo paso en la creacion de una nueva generacion.
           Se asignan aptitudes y se seleccionan los sobrevivientes.
        '''
        # asigno aptitudes
        print 'asignando aptitudes...'
        for i in xrange(len(self.population)):
            individual = self.population[i]
            print 'procesando individuo %d... -> %s' % (i, individual)
            # el elemento de la posicion 1 de la tupla es un flag que determina
            # si se debe actualizar la aptitud del elemento.
            # en el caso de que la constante RECALCULATE_INDIVIDUALS sea True siempre
            # se recalcula la aptitud de todos los individuos en todas las generaciones 
            
            if BlackTablutAgentEvolution.RECALCULATE_INDIVIDUALS:
                self.population[i] = self._update_individual(individual)
                print 'aptitud=%d' % self.population[i][0]
            elif individual[1]:
                self.population[i] = self._update_individual(individual)
                print 'aptitud=%d' % self.population[i][0]
            print 'individuo %d procesado' % i
        # una vez asignadas todas las aptitudes a la primera posicion de las tuplas
        # ordeno con el sort por defecto de python
        print 'ordenando...'
        self.population.sort(reverse=True)
        # registro el valor de aptitud del individuo mas apto
        self.max_fitness_values.append(self.population[0][0])
        # elimino los peores individuos
        print 'eliminando losers...'
        self.population = self.population[:BlackTablutAgentEvolution.POPULATION_SIZE]
        
    def _update_individual(self, individual):
        '''Asigna la aptitud a un individuo.
        '''
        #FIXME!
        '''
        fitness_func_res = self.fitness_function(individual[2])
        if individual[0]:
            new_fitness = (fitness_func_res + individual[0])/2
        else:
            new_fitness = fitness_func_res
        '''
        return (self.fitness_function(individual[2]), False, individual[2])
    
    def generations(self):
        ''' Generator que retorna una a una las generaciones de la evolucion.
        '''
        generation_num = 0
        while generation_num < BlackTablutAgentEvolution.MAX_GENERATIONS:
            yield (generation_num, self.population)
            self.next_generation()
            generation_num+=1
        yield (generation_num, self.population)
    
    def fitness_function(self, coefficients):
        '''Es la funcion de aptitud, genera un contest (la cantidad de partidos depende de la 
        variable GAMES_PER_CONTEST) y utiliza una funcion matematica basada en las estadisticas
        recopiladas para generar la aptitud de un individuo.
        Retorna la aptitud del individuo.
        '''
        treshold = int(TablutContest.GAMES_PER_CONTEST / 2) # 
        gamma = TablutContest.GAMES_PER_CONTEST*100 # constante de ponderacion de victoria 
        contest = TablutContest('Tablut', coefficients)
        for _ in contest.run():
            pass
        stats = contest.stats
        wins = stats["agent_wins"]
        moves_count = stats["agent_moves"]
        if wins > treshold:
            return wins*gamma - moves_count
        else:
            return wins*gamma + moves_count
    
    def total_fitness(self):
        '''Retorna la sumatoria de aptitudes de la poblacion en una generacion dada.
        '''
        fs = []
        for ind in self.population:
            fs.append(ind[0])
        return sum(fs)

def print_configurations():
    ''' Imprime las configuraciones de la evolucion.
    '''
    print '\n<<<CONFIGURACIONES>>>'
    print 'INITIAL_POPULATION_SIZE = %d' % BlackTablutAgentEvolution.INITIAL_POPULATION_SIZE
    print 'COEFFICIENTS_INTERVAL = %s' % str(BlackTablutAgentEvolution.COEFFICIENTS_INTERVAL)
    print 'GAMES_PER_CONTEST = %d' % TablutContest.GAMES_PER_CONTEST
    print 'INDIVIDUALS_TO_MATE = %d' % BlackTablutAgentEvolution.INDIVIDUALS_TO_MATE
    print 'RECALCULATE_INDIVIDUALS = %s' % str(BlackTablutAgentEvolution.RECALCULATE_INDIVIDUALS)
    print 'MAX_GENERATIONS = %d' % BlackTablutAgentEvolution.MAX_GENERATIONS
    print 'MUTATION_VICTIMS = %d' % BlackTablutAgentEvolution.MUTATION_VICTIMS
    print 'POPULATION_SIZE = %d' % BlackTablutAgentEvolution.POPULATION_SIZE

#Ejecuta la evolucion y logea informacion importante sobre la misma.
if __name__ == '__main__':
    init_time = time.time()
    timestamp = time.strftime('%Y%m%d%H%M')
    log_file = open('log_%s.txt' % timestamp, 'w')
    evo = BlackTablutAgentEvolution()
    for num, pop in evo.generations():
        log_file = open('log_%s.txt' % timestamp, 'a')
        saveout = sys.stdout
        sys.stdout = log_file
        print '<<<%d>>> Mejor individuo -> %s' % (num, str(pop[0]))
        print 'poblacion = %s' % str(pop)
        print 'sumatoria_aptitud = %d' % evo.total_fitness()
        print 'funcion_aptitud_parcial = %s' % str(evo.max_fitness_values)
        sys.stdout = saveout
        log_file.close()
    log_file = open('log_%s.txt' % timestamp, 'a')
    saveout = sys.stdout
    sys.stdout = log_file
    print_configurations()
    diff = time.time() - init_time
    print 'La ejecucion finalizo luego de %d segundos' % diff
    sys.stdout = saveout
    log_file.close()
    print 'La ejecucion finalizo luego de %d segundos' % diff
    